﻿namespace wToggl
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Runtime.Serialization;
    using System.Runtime.Serialization.Json;
    using System.Threading;
    using System.Threading.Tasks;

    using Windows.Networking.Connectivity;
    using Windows.Storage;

    using wToggle.Platform;
    using wToggle.Platform.Data;
    using wToggle.Platform.Interfaces;

    using XPlatUtils;

    internal class PhoneCacheProvider : ICacheProvider
    {
        const string CacheFileName = "cache.json";

        private const int CacheTimeout = 15;

        private OfflineCache cache;

        private readonly Semaphore sem = new Semaphore(1, 1);

        private StorageFile file;

        public async Task Initialize()
        {
            this.file = await OpenCacheFile(CacheFileName);

            if (this.file == null)
            {
                this.cache = new OfflineCache();
                return;
            }

            var serializer = new DataContractJsonSerializer(typeof(OfflineCache));

            using (var stream = await this.file.OpenStreamForReadAsync())
            {
                try
                {
                    this.cache = (OfflineCache)serializer.ReadObject(stream);
                }
                catch (SerializationException)
                {
                    this.cache = new OfflineCache();
                }
            }

            if (this.cache == null)
            {
                this.cache = new OfflineCache();
            }
        }

        private async Task<bool> FlushWritesCore()
        {
            if (!Toggly.HasInternetAccess())
            {
                return true;
            }

            try
            {
                this.sem.WaitOne();

                if (!this.HasPendingWrites(withLock: false))
                {
                    return true;
                }

                var res = await this.FlushTypedWriteCache<Workspace>();
                var result = res.Success;

                res = await this.FlushTypedWriteCache<Client>();
                result = result && res.Success;

                // switch temporary client IDs for real ones
                foreach (var key in res.Keys)
                {
                    var projects = this.GetWriteCacheCollection<Project>().Where(r => r.Entity.ClientId == key.Item1);
                    foreach (var p in projects)
                    {
                        p.Entity.ClientId = key.Item2;
                    }
                }

                res = await this.FlushTypedWriteCache<Project>();
                result = result && res.Success;

                // switch temporary project IDs for real ones
                foreach (var key in res.Keys)
                {
                    var projects = this.GetWriteCacheCollection<TimeEntry>().Where(r => r.Entity.ProjectId == key.Item1);
                    foreach (var p in projects)
                    {
                        p.Entity.ProjectId = key.Item2;
                    }
                }
                
                // tags work on names, so no need to fix IDs
                await this.FlushTypedWriteCache<Tag>();
                result = result && res.Success;

                await this.FlushTypedWriteCache<TimeEntry>();

                await this.SaveCore();
                return result;
            }
            finally
            {
                this.sem.Release();
            }
        }

        private async Task<FlushResult> FlushTypedWriteCache<TEntity>() where TEntity : class, IEntity
        {
            var result = new FlushResult { Success = true };

            var provider = this.ResolveProvider<TEntity>();
            var readCache = this.GetReadCacheCollection<TEntity>();
            var writeCache = this.GetWriteCacheCollection<TEntity>();
            foreach (var record in writeCache.ToArray())
            {
                var oldId = record.Entity.Id;
                switch (record.Type)
                {
                    case WriteType.Create:
                       
                        record.Entity.Id = 0; // remove temporary ID
                        var n = await provider.Create(record.Entity, false);

                        if (n != null)
                        {
                            writeCache.Remove(record);
                            readCache.Remove(oldId);
                            result.Keys.Add(new Tuple<ulong, ulong>(oldId, n.Id));
                        }
                        else
                        {
                            record.Entity.Id = oldId;
                            result.Success = false;
                        }

                        break;
                    case WriteType.Delete:
                        var d = await provider.Delete(oldId, false);

                        if (d)
                        {
                            writeCache.Remove(record);
                        }

                        break;
                    case WriteType.Update:
                        var u = await provider.Update(record.Entity, false);

                        if (u != null)
                        {
                            writeCache.Remove(record);
                        }

                        break;
                }
            }

            return result;
        }

        private IBasicDataProvider<TEntity> ResolveProvider<TEntity>() where TEntity : class, IEntity
        {
            var type = typeof(TEntity);

            if (type == typeof(Workspace))
            {
                return ServiceContainer.Resolve<IWorkspaceProvider>() as IBasicDataProvider<TEntity>;
            }
            else if (type == typeof(Project))
            {
                return ServiceContainer.Resolve<IProjectProvider>() as IBasicDataProvider<TEntity>;
            }
            else if (type == typeof(Client))
            {
                return ServiceContainer.Resolve<IClientProvider>() as IBasicDataProvider<TEntity>;
            }
            else if (type == typeof(Tag))
            {
                return ServiceContainer.Resolve<ITagProvider>() as IBasicDataProvider<TEntity>;
            }
            else if (type == typeof(TimeEntry))
            {
                return ServiceContainer.Resolve<ITimeEntryProvider>() as IBasicDataProvider<TEntity>;
            }

            return null;
        }

        private static async Task<StorageFile> OpenCacheFile(string cacheFileName)
        {
            StorageFile file = null;

            try
            {
                file = await ApplicationData.Current.LocalFolder.GetFileAsync(cacheFileName);
            }
            catch (IOException)
            {
            }

            if (file == null)
            
            {
                try
                {
                    file = await ApplicationData.Current.LocalFolder.CreateFileAsync(cacheFileName);
                }
                catch (IOException)
                {
                }
            }

            return file;
        }

        public TEntity LoadEntity<TEntity>(ulong key) where TEntity : class, IEntity
        {
            try
            {
                this.sem.WaitOne();
                var type = typeof(TEntity);

                if (type == typeof(Workspace))
                {
                    var record = this.cache.Workspaces.FindKeyOrNull(key);

                    if (record == null || record.IsOlderThanMinutes(CacheTimeout))
                    {
                        return null;
                    }

                    return record.Entity as TEntity;
                }
                else if (type == typeof(Project))
                {
                    var record = this.cache.Projects.FindKeyOrNull(key);

                    if (record == null || record.IsOlderThanMinutes(CacheTimeout))
                    {
                        return null;
                    }

                    return record.Entity as TEntity;
                }
                else if (type == typeof(Client))
                {
                    var record = this.cache.Clients.FindKeyOrNull(key);

                    if (record == null || record.IsOlderThanMinutes(CacheTimeout))
                    {
                        return null;
                    }

                    return record.Entity as TEntity;
                }
                else if (type == typeof(Tag))
                {
                    var record = this.cache.Tags.FindKeyOrNull(key);

                    if (record == null || record.IsOlderThanMinutes(CacheTimeout))
                    {
                        return null;
                    }

                    return record.Entity as TEntity;
                }
                else if (type == typeof(TimeEntry))
                {
                    var record = this.cache.TimeEntries.FindKeyOrNull(key);

                    if (record == null || record.IsOlderThanMinutes(CacheTimeout))
                    {
                        return null;
                    }

                    return record.Entity as TEntity;
                }
                else if (type == typeof(UserData))
                {
                    var record = this.cache.UserData;

                    if (record == null)
                    {
                        return null;
                    }

                    return record.Entity as TEntity;
                }

                return null;
            }
            finally
            {
                this.sem.Release();
            }
        }

        public ICollection<TEntity> LoadEntities<TEntity>() where TEntity : class, IEntity
        {
            try
            {
                this.sem.WaitOne();
                var type = typeof(TEntity);

                if (type == typeof(Workspace))
                {
                    if (!this.cache.Workspaces.Any() || this.cache.Workspaces.Any(w => w.Value.IsOlderThanMinutes(CacheTimeout)))
                    {
                        return null;
                    }
                    else
                    {
                        return this.cache.Workspaces.Select(w => w.Value.Entity).Cast<TEntity>().ToList();
                    }
                }
                else if (type == typeof(Project))
                {
                    if (!this.cache.Projects.Any() || this.cache.Projects.Any(w => w.Value.IsOlderThanMinutes(CacheTimeout)))
                    {
                        return null;
                    }
                    else
                    {
                        return this.cache.Projects.Select(w => w.Value.Entity).Cast<TEntity>().ToList();
                    }
                }
                else if (type == typeof(Client))
                {
                    if (!this.cache.Clients.Any() || this.cache.Clients.Any(w => w.Value.IsOlderThanMinutes(CacheTimeout)))
                    {
                        return null;
                    }
                    else
                    {
                        return this.cache.Clients.Select(w => w.Value.Entity).Cast<TEntity>().ToList();
                    }
                }
                else if (type == typeof(Tag))
                {
                    if (!this.cache.Tags.Any() || this.cache.Tags.Any(w => w.Value.IsOlderThanMinutes(CacheTimeout)))
                    {
                        return null;
                    }
                    else
                    {
                        return this.cache.Tags.Select(w => w.Value.Entity).Cast<TEntity>().ToList();
                    }
                }
                else if (type == typeof(TimeEntry))
                {
                    if (!this.cache.TimeEntries.Any() || this.cache.TimeEntries.Any(w => w.Value.IsOlderThanMinutes(CacheTimeout)))
                    {
                        return null;
                    }
                    else
                    {
                        return this.cache.TimeEntries.Select(w => w.Value.Entity).Cast<TEntity>().ToList();
                    }
                }

                return null;
            }
            finally
            {
                this.sem.Release();
            }
        }

        public async Task StoreEntity<TEntity>(TEntity entity, bool withLock) where TEntity : class, IEntity
        {
            if (entity == null)
            {
                return;
            }

            try
            {
                if (withLock)
                {
                    this.sem.WaitOne();
                }
               
                this.StoreEntityCore(entity);
                await this.SaveCore();
            }
            finally
            {
                if (withLock)
                {
                    this.sem.Release();
                }
            }
        }

        private void StoreEntityCore<TEntity>(TEntity entity) where TEntity : class, IEntity
        {         
            var type = typeof(TEntity);

            if (type == typeof(Workspace))
            {
                var record = this.cache.Workspaces.FindKeyOrNull(entity.Id);

                if (record == null)
                {
                    this.cache.Workspaces.Add(entity.Id, new CacheRecord<Workspace>(entity as Workspace));
                }
                else
                {
                    record.Entity = entity as Workspace;
                }
            }
            else if (type == typeof(Project))
            {
                var record = this.cache.Projects.FindKeyOrNull(entity.Id);

                if (record == null)
                {
                    this.cache.Projects.Add(entity.Id, new CacheRecord<Project>(entity as Project));
                }
                else
                {
                    record.Entity = entity as Project;
                }
            }
            else if (type == typeof(Client))
            {
                var record = this.cache.Clients.FindKeyOrNull(entity.Id);

                if (record == null)
                {
                    this.cache.Clients.Add(entity.Id, new CacheRecord<Client>(entity as Client));
                }
                else
                {
                    record.Entity = entity as Client;
                }
            }
            else if (type == typeof(Tag))
            {
                var record = this.cache.Tags.FindKeyOrNull(entity.Id);

                if (record == null)
                {
                    this.cache.Tags.Add(entity.Id, new CacheRecord<Tag>(entity as Tag));
                }
                else
                {
                    record.Entity = entity as Tag;
                }
            }
            else if (type == typeof(TimeEntry))
            {
                var record = this.cache.TimeEntries.FindKeyOrNull(entity.Id);

                if (record == null)
                {
                    this.cache.TimeEntries.Add(entity.Id, new CacheRecord<TimeEntry>(entity as TimeEntry));
                }
                else
                {
                    record.Entity = entity as TimeEntry;
                }
            }
            else if (type == typeof(UserData))
            {
                var record = this.cache.UserData;

                if (record == null)
                {
                    this.cache.UserData = new CacheRecord<UserData>(entity as UserData);
                }
                else
                {
                    record.Entity = entity as UserData;
                }
            }
        }

        public async Task StoreEntities<TEntity>(IEnumerable<TEntity> entities) where TEntity : class, IEntity
        {
            try
            {
                this.sem.WaitOne();
                if (entities == null)
                {
                    return;
                }

                foreach (var entity in entities)
                {
                    this.StoreEntityCore(entity);
                }

                await this.SaveCore();
            }
            finally
            {
                this.sem.Release();
            }
        }

        public async Task Save()
        {
            if (this.file == null)
            {
                return;
            }

            try
            {
                this.sem.WaitOne();
                await this.SaveCore();
            }
            finally
            {
                this.sem.Release();
            }
        }

        private async Task SaveCore()
        {
            var serializer = new DataContractJsonSerializer(typeof(OfflineCache));

            var stream =
                await
                ApplicationData.Current.LocalFolder.OpenStreamForWriteAsync(
                    CacheFileName,
                    CreationCollisionOption.ReplaceExisting);

            using (stream)
            {
                serializer.WriteObject(stream, this.cache);
            }
        }

        public bool HasPendingWrites(bool withLock = true)
        {
            try
            {
                if (withLock)
                {
                    this.sem.WaitOne();
                }

                return this.cache.WorkspaceWriteCache.Any() || this.cache.ClientWriteCache.Any()
                       || this.cache.ProjectWriteCache.Any() || this.cache.TagWriteCache.Any()
                       || this.cache.TimeEntryWriteCache.Any();
            }
            finally
            {
                if (withLock)
                {
                    this.sem.Release();
                }
            }
        }

        public async Task Purge()
        {
            try
            {
                this.sem.WaitOne();
                this.cache = new OfflineCache();
                await this.SaveCore();
            }
            finally
            {
                this.sem.Release();
            }
        }

        private Dictionary<ulong, CacheRecord<TEntity>> GetReadCacheCollection<TEntity>() where TEntity : class, IEntity
        {
            var type = typeof(TEntity);

            if (type == typeof(Workspace))
            {
                return this.cache.Workspaces as Dictionary<ulong, CacheRecord<TEntity>>;
            }
            else if (type == typeof(Project))
            {
                return this.cache.Projects as Dictionary<ulong, CacheRecord<TEntity>>;
            }
            else if (type == typeof(Client))
            {
                return this.cache.Clients as Dictionary<ulong, CacheRecord<TEntity>>;
            }
            else if (type == typeof(Tag))
            {
                return this.cache.Tags as Dictionary<ulong, CacheRecord<TEntity>>;
            }
            else if (type == typeof(TimeEntry))
            {
                return this.cache.TimeEntries as Dictionary<ulong, CacheRecord<TEntity>>;
            }

            return null;
        }

        private List<WriteRecord<TEntity>> GetWriteCacheCollection<TEntity>() where TEntity : class, IEntity
        {
            var type = typeof(TEntity);

            if (type == typeof(Workspace))
            {
                return this.cache.WorkspaceWriteCache as List<WriteRecord<TEntity>>;
            }
            else if (type == typeof(Project))
            {
                return this.cache.ProjectWriteCache as List<WriteRecord<TEntity>>;
            }
            else if (type == typeof(Client))
            {
                return this.cache.ClientWriteCache as List<WriteRecord<TEntity>>;
            }
            else if (type == typeof(Tag))
            {
                return this.cache.TagWriteCache as List<WriteRecord<TEntity>>;
            }
            else if (type == typeof(TimeEntry))
            {
                return this.cache.TimeEntryWriteCache as List<WriteRecord<TEntity>>;
            }

            return null;
        }

        public async Task StoreEntryWrite<TEntity>(TEntity entity, WriteType writeType, bool withLock) where TEntity : class,  IEntity
        {
            if (entity == null)
            {
                return;
            }

            try
            {
                if (withLock)
                {
                    this.sem.WaitOne();
                }

                if (writeType == WriteType.Create)
                {
                    this.CacheCreation(entity, writeType);
                }
                else
                {
                    if (entity.Id <= 0)
                    {
                        return;
                    }

                    var writeCache = this.GetWriteCacheCollection<TEntity>();
                    var readCache = this.GetReadCacheCollection<TEntity>();

                    var existing = writeCache.FirstOrDefault(r => r.Entity.Id == entity.Id && r.Type == writeType);

                    if (existing == null)
                    {
                        writeCache.Add(new WriteRecord<TEntity>(entity, writeType));
                    }
                    else
                    {
                        writeCache.Remove(existing);
                        writeCache.Add(new WriteRecord<TEntity>(entity, writeType));
                    }
                   
                    if (writeType == WriteType.Delete)
                    {
                        var cachedCreate = writeCache.FirstOrDefault(r => r.Entity.Id == entity.Id && r.Type == WriteType.Create);

                        if (cachedCreate != null)
                        {
                            // created in cache - we can remove everything
                            writeCache.RemoveAll(r => r.Entity.Id == entity.Id); 
                        }
                        else
                        {
                            // not created in cache - must leave delete behind
                            writeCache.RemoveAll(r => r.Entity.Id == entity.Id && r.Type != WriteType.Delete); 
                        }                                         
                    }

                    CacheRecord<TEntity> cachedEntry;
                    if (readCache.TryGetValue(entity.Id, out cachedEntry))
                    {
                        if (writeType == WriteType.Delete)
                        {
                            readCache.Remove(cachedEntry.Entity.Id);
                        }
                        else if (writeType == WriteType.Update)
                        {
                            cachedEntry.Entity = entity;
                        }
                    }
                }

                await this.SaveCore();
            }
            finally
            {
                if (withLock)
                {
                    this.sem.Release();
                }
            }
        }

        private void CacheCreation<TEntity>(TEntity entity, WriteType writeType) where TEntity : class, IEntity
        {
            var readCache = this.GetReadCacheCollection<TEntity>();
            var writeCache = this.GetWriteCacheCollection<TEntity>();

            writeCache.Add(new WriteRecord<TEntity>(entity, writeType));

            if (readCache.Any())
            {
                var max = readCache.Max(kvp => kvp.Key + 1);
                entity.Id = max;
            }
            else
            {
                entity.Id = 1;
            }
            
            readCache.Add(entity.Id, new CacheRecord<TEntity>(entity));
        }

        public async Task<bool> FlushWrites()
        {
            return await this.FlushWritesCore();
        }

        public async Task RemoveEntity<TEntity>(ulong id, bool withLock) where TEntity : class, IEntity
        {
            if (id == 0)
            {
                return;
            }

            try
            {
                if (withLock)
                {
                    this.sem.WaitOne();
                }

                this.RemoveCore<TEntity>(id);

                await this.SaveCore();
            }
            finally
            {
                if (withLock)
                {
                    this.sem.Release();
                }
            }
        }

        private void RemoveCore<TEntity>(ulong id) where TEntity : class, IEntity
        {
            var type = typeof(TEntity);
           
            if (type == typeof(UserData))
            {
                this.cache.UserData = null;
            }
            else
            {
                this.GetReadCacheCollection<TEntity>().Remove(id);
            }
        }

        private class FlushResult
        {
            public FlushResult()
            {
                this.Keys = new List<Tuple<ulong, ulong>>();
            }

            public List<Tuple<ulong, ulong>> Keys { get; set; }

            public bool Success { get; set; }
        }
    }
}
